package dk.itu.spvc.tourtracker.gui;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.util.EntityUtils;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.Toast;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;

import dk.itu.spvc.tourtracker.model.CurrentState;
import dk.itu.spvc.tourtracker.model.Note;
import dk.itu.spvc.tourtracker.model.Point;
import dk.itu.spvc.tourtracker.model.PointType;
import dk.itu.spvc.tourtracker.model.ResponseListener;
import dk.itu.spvc.tourtracker.model.Trip;
import dk.itu.spvc.tourtracker.service.ServiceLocalSQL;
import dk.itu.spvc.tourtracker.service.ServiceRemoteSQL;

public class Map extends MapActivity 
	implements LocationListener, Observer, OnGestureListener, OnDoubleTapListener, ResponseListener 
	{
	
	// CONSTANTS -------------------------------------------------------------

	// Debug
	private static final String TAG = "TourTracker";
	private static final boolean D = true;
	private static final boolean DEBUG = false;
	
	// Map Settings
	private static final int INITIAL_MAP_ZOOM = 16;
	private static final int MIN_LOCATION_UPDATE_TIME = 5000; // ms
	private static final int MIN_LOCATION_UPDATE_DISTANCE = 10; // m
	
	// Intent Requests
	public static final int REQUEST_CREATE_TRIP = 0;
	public static final int REQUEST_UPLOAD = 1;
	public static final int REQUEST_CREATE_PIN = 2;
	public static final int REQUEST_PIN = 3;
	public static final int REQUEST_ENABLE_LOCATION_PROVIDER = 4;
	public static final int REQUEST_LOAD_TRIP = 5;
	
	// PRIVATE FIELDS ---------------------------------------------------------

	private MapView mapView;
	private MapController mapController;
	private LocationManager locationManager;
	private LocationOverlay locationOverlay;
	private PathOverlay pathOverlay;
	private DisplayOverlay displayOverlay;
	private PinOverlay pinOverlay;
	
	private boolean isMenuCreated = false;
	private MenuItem menuItemRecord;
	private MenuItem menuItemPauseResume;
	private MenuItem menuItemStop;
	private MenuItem menuItemUpload;
	private MenuItem menuItemTrips;
	private MenuItem menuItemExit;
	
	private String locationProvider;
	private CurrentState currentState;
	
	private ServiceRemoteSQL srvc_remote = ServiceRemoteSQL.getService();
	private ServiceLocalSQL srvc_local = ServiceLocalSQL.getService(this);

	private GestureDetector gestureDetector;

	// OVERRIDES --------------------------------------------------------------

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		if (D)
			Log.d(TAG, "Map - Created");

		setContentView(R.layout.main);

		initMap();

		// Init location manager & subscribe to location updates
		locationManager = (LocationManager) this.getSystemService(LOCATION_SERVICE);
		
		// Init gesture detector
		gestureDetector = new GestureDetector(getBaseContext(), this);
		gestureDetector.setOnDoubleTapListener(this);
		
		// Init current state
		currentState = srvc_local.getCurrentState();
		if (currentState == null) {
			currentState = new CurrentState(-1, CurrentState.VIEW);
		}

		// Add observer
		currentState.addObserver(this);
	}

	@Override
	public void onStart() {
		super.onStart();

		// Check for internet access
		if (!isOnline()) {
			Toast.makeText(getApplicationContext(), "Internet connection is not available", Toast.LENGTH_LONG).show();				
		}
		 
		// Get current state vars
		int tripId = currentState.getTripId();			
		int state = currentState.getState();
		String tripName = (tripId != -1) ? srvc_local.getTrip(tripId).getTripName() : "";	
		
		if (D) Log.d(TAG, "Map - Started - CurrentState: tripId=" + tripId + ", state=" + state);
				
		// Init display overlay
		displayOverlay.setText(tripName, state, locationProvider);
	}

	@Override
	public void onPause() {
		super.onPause();
		
		//if (D) Log.d(TAG, "Map - Paused");

	}

	@Override
	public void onResume() {
		super.onResume();

		// Subscribe to location updates
		if (locationProvider == null) {
			requestLocationUpdates();
		}
		
		//if (D) Log.d(TAG, "Map - Resumed");
	}

	@Override
	public void onStop() {
		super.onStop();
		
		// Remove location updates to save resources if not recording
		if (currentState.getState() != CurrentState.RECORD) {
			removeLocationUpdates();
		}
		
		if (D) Log.d(TAG, "Map - Stopped");
	}

	@Override
	public void onDestroy() {
		if (D)
			Log.d(TAG, "Map - Destroyed");
		super.onDestroy();
	}

	public void onResponseReceived(HttpResponse response) {
		try {
			if (D)
				Log.d(TAG, EntityUtils.toString(response.getEntity()));
		} catch (ParseException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	protected boolean isRouteDisplayed() {
		return false;
	}

	// PRIVATE METHODS --------------------------------------------------------
	
	private boolean isOnline() {
		try {
			ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
			NetworkInfo ni = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
			if (ni.isAvailable()) {
				if (ni.isConnectedOrConnecting()) {
					return true;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

	private void initMenu() {

		// Init menu buttons depending on state

		if (isMenuCreated == false || currentState == null) return;
		
		switch (currentState.getState()) {
			case (CurrentState.VIEW):				
				menuItemRecord.setEnabled(true);
				menuItemPauseResume.setEnabled(false);
				menuItemPauseResume.setTitle("Pause");
				menuItemStop.setEnabled(false);
				menuItemUpload.setEnabled(isThereAnyTripsRecorded());				
				break;
	
			case (CurrentState.RECORD):
				menuItemRecord.setEnabled(false);
				menuItemPauseResume.setEnabled(true);
				menuItemPauseResume.setTitle("Pause");
				menuItemStop.setEnabled(true);
				menuItemUpload.setEnabled(false);
				break;
			
			case (CurrentState.PAUSED):
				menuItemRecord.setEnabled(false);
				menuItemPauseResume.setEnabled(true);
				menuItemPauseResume.setTitle("Resume");
				menuItemStop.setEnabled(true);
				menuItemUpload.setEnabled(false);				
				break;
		}
	}

	private void initMap() {

		// Init map view
		mapView = (MapView) findViewById(R.id.mapview);
		mapView.setBuiltInZoomControls(true);
		mapView.setSatellite(false);
		mapView.setStreetView(false);
		mapView.setClickable(true);
		mapView.setEnabled(true);

		// Init map controller
		mapController = mapView.getController();

		// Init location overlay
		Drawable defaultLocationMarker = getResources().getDrawable(R.drawable.location_marker);
		locationOverlay = new LocationOverlay(defaultLocationMarker);
		mapView.getOverlays().add(locationOverlay);
		
		// Run on first fix
		locationOverlay.runOnFirstFix(new Runnable() {
			public void run() {
				// Animate & zoom to current location
				double lat = locationOverlay.getLocation().getLatitude();
				double lng = locationOverlay.getLocation().getLongitude();
				mapController.animateTo(new GeoPoint((int) (lat * 1E6), (int) (lng * 1E6)));
				mapController.setZoom(INITIAL_MAP_ZOOM);
			}
		});
		
		// Init path overlay
		pathOverlay = new PathOverlay();
		mapView.getOverlays().add(pathOverlay);	
		
		// Init pins overlay
		pinOverlay = new PinOverlay(this, getResources().getDrawable(R.drawable.note));
		mapView.getOverlays().add(pinOverlay);
		
		// Init display overlay
		displayOverlay = new DisplayOverlay();
		mapView.getOverlays().add(displayOverlay);
	}

	private void loadTrip(int tripId) {
		
		// Clear map
		clearMap();	
		
		// Add path
		pathOverlay.addPoints(srvc_local.getPoints(tripId, PointType.PATH));
		
		// Add pin-points
		List<Point> points = srvc_local.getPoints(tripId, PointType.PIN);
		List<Note> notes = getNotes(points);				
		pinOverlay.addPins(points, notes);
		
		currentState.setTripId(tripId);
		currentState.setState(CurrentState.VIEW);
	}
	
	private void clearMap () {
		pinOverlay.clear();
		pathOverlay.clear();
		mapView.invalidate();
	}
	
	private void requestLocationUpdates() {

		// TODO use GPS only
		
		/* Currently only GPS will be used.
		 * Can be extended to use default network or custom skyhook or other location provider.
		 */
		
		if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
			locationProvider = LocationManager.GPS_PROVIDER;
	    }
		
		if (DEBUG) {
			locationProvider = LocationManager.NETWORK_PROVIDER;
		}
		
        if (locationProvider != null) {
        	locationManager.requestLocationUpdates(locationProvider, MIN_LOCATION_UPDATE_TIME, MIN_LOCATION_UPDATE_DISTANCE, this);	       	
        }
        else {
        	// No location provider available
        	requestUserEnableGPSProvider(false);
        }        
	}
	
	private void removeLocationUpdates () {
		locationManager.removeUpdates(this);
		locationProvider = null;
	}

	private void requestUserEnableGPSProvider(boolean isRequirement) {
		
		// TODO if isRequirement==true:
		// used when starting recording - if no gps provider - cancel recording
		
	    final AlertDialog.Builder builder = new AlertDialog.Builder(this);
	    builder.setMessage("GPS seems to be disabled, would you like to enable it now?")
	           .setCancelable(!isRequirement)
	           .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
	               public void onClick(final DialogInterface dialog, final int id) {
	            	   startActivityForResult(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0);
	               }
	           })
	           .setNegativeButton("No", new DialogInterface.OnClickListener() {
	               public void onClick(final DialogInterface dialog, final int id) {
	                    dialog.cancel();
	               }
	           });
	    final AlertDialog alert = builder.create();
	    alert.show();
	}
			
	private boolean isThereAnyTripsRecorded() {
		
		// Checks for at least one trip with some recorded data

		List<Trip> trips = srvc_local.getTrips(true);
		
		boolean doesAtLeastOneTripHavePoints = false;
		for (Trip trip: trips) {
			if (doesTripHavePoints(trip.getTripId())) {
				doesAtLeastOneTripHavePoints = true;
			}
		}
				
		return doesAtLeastOneTripHavePoints;
	}
	
	private boolean doesTripHavePoints(int tripId) {
		if (srvc_local.getPoints(tripId, PointType.PATH).size() > 1 || 
			srvc_local.getPoints(tripId, PointType.PIN).size() > 0) {
			return true;
		}
		return false;
	}

	private Note getNote (Point point) {
		
		// Hack
		
		List<Note> pointNotes = srvc_local.getNotes(point.getPointId());
		if (pointNotes.size() > 0) {
			return pointNotes.get(0);
		}
		
		// Should never occure, but just in case
		return new Note(currentState.getTripId(), -1, point.getPointId(), "");
	}
	
	private List<Note> getNotes(List<Point> points) {
		
		// Hack - should be done in database
		
		List<Note> notes = new ArrayList<Note>();
		for (Point point : points) {
			notes.add(getNote(point));
		}
		return notes;
	}
	
	// CURRENT STATE OBSERVER -------------------------------------------------
	
	public void update(Observable observable, Object data) {

		if (data instanceof CurrentState) {
		
			int tripId = currentState.getTripId();			
			int state = currentState.getState();			
			String tripName = (tripId != -1) ? srvc_local.getTrip(tripId).getTripName() : "";						
						
			// Save new state to database	
			srvc_local.setCurrentState(tripId, state);
			
			// Update menu
			initMenu();
			
			// Update output text			
			displayOverlay.setText(tripName, state, locationProvider);
			
			// Update pin overlay
			pinOverlay.setState(state);
			
			mapView.invalidate();
			
			Log.d(TAG, "Map - New state - " + currentState.getNamedState());
		}

	}

	// LOCATION LISTENER IMPLEMENTATION ---------------------------------------

	public void onLocationChanged(Location location) {		
		
		// Get vars
		int tripId = currentState.getTripId();
		float accuracy = location.getAccuracy();
		String provider = location.getProvider();
		double lat = location.getLatitude();
		double lng = location.getLongitude();			
		
		// Update location overlay
		locationOverlay.setLocation(location);					
		
		if (D) Log.i(TAG, "Map - New location - (" + lat + ", " + lng + ") - accuracy=" + accuracy + " provider=" + provider);
		
		if (currentState.getState() == CurrentState.RECORD) {
		
			// Animate
			mapController.animateTo(new GeoPoint((int) (lat * 1E6), (int) (lng * 1E6)));
			
			// Update path overlay				
			pathOverlay.addPoint(lat, lng);
			
			// Update local db
			srvc_local.addPoint(tripId, lng, lat, PointType.PATH);
			
			if (D) Log.i(TAG, "Map - New location added to route");
		}
		
	}

	public void onProviderDisabled(String arg0) {
		if (D)
			Log.d(TAG, "Map - Location provider disabled - " + arg0);

		// Remove location updates to save resources
		removeLocationUpdates();
	}

	public void onProviderEnabled(String arg0) {
		if (D) Log.d(TAG, "Map - Location provider enabled - " + arg0);

		// Subscribe to location updates
		requestLocationUpdates();
	}

	public void onStatusChanged(String arg0, int arg1, Bundle arg2) {

		String status = null;
		switch (arg1) {
		case LocationProvider.AVAILABLE:
			status = "AVAILABLE";
		case LocationProvider.OUT_OF_SERVICE:
			status = "OUT_OF_SERVICE";
		case LocationProvider.TEMPORARILY_UNAVAILABLE:
			status = "TEMPORARILY_UNAVAILABLE";
		}

		if (D)
			Log.d(TAG, "Map - Location provider status changed - " + arg0 + " - " + status);
	}

	// INTENT HANDLING --------------------------------------------------------

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {

		super.onActivityResult(requestCode, resultCode, data);

		switch (requestCode) {

		case REQUEST_CREATE_TRIP:

			if (resultCode == RESULT_OK) {

				String tripName = data.getStringExtra("tripName");
				long tripId = srvc_local.addTrip(tripName);

				if (tripId != -1) {					
					
					// Success - start recording
					currentState.setTripId((int) tripId);
					currentState.setState(CurrentState.RECORD);
					
					// Clear the map
					clearMap();
					
					Log.d(TAG, "Map - Trip created, new tripId=" + tripId);
					
				} else {
					// Failed
					Log.d(TAG, "Map - Trip creation request failed");
				}
			}

			break;

		case REQUEST_UPLOAD:

			if (resultCode == RESULT_OK) {
				currentState.setTripId(-1);
				currentState.setState(CurrentState.VIEW);									
			} else {				
				Log.d(TAG, "Map - Upload request failed");
			}

			break;

		case REQUEST_CREATE_PIN:

			if (resultCode == RESULT_OK) {

				String action = data.getStringExtra("action");
				String pinNoteText = data.getStringExtra("pinNoteText");

				if (action.contentEquals(CreatePinActivity.ACTION_CREATE)) {
					
					int lat = data.getIntExtra("latitude", -1);
					int lng = data.getIntExtra("longitude", -1);
					int tripId = currentState.getTripId();
					
					long pointId = srvc_local.addPoint(tripId, lng / 1E6, lat / 1E6, PointType.PIN);
					if (pointId != -1) {
						long noteId = srvc_local.addNote((int) pointId, currentState.getTripId(), pinNoteText);
						if (noteId != -1) {							
							Point point = new Point(pointId, tripId, lng / 1E6, lat / 1E6, PointType.PIN);
							Note note = new Note(tripId, noteId, pointId, pinNoteText);
							pinOverlay.addPin(point, note);							
						}
					}
					
					mapView.invalidate();
					
				} else if (action.contentEquals(CreatePinActivity.ACTION_EDIT)) {

					int pointIndex = data.getIntExtra("pointIndex", -1);
					PinOverlayItem item = pinOverlay.getItem(pointIndex);

					if (srvc_local.updateNote((int) item.getPointId(), pinNoteText)) {
						pinOverlay.deleteOverlay(pointIndex);						
						PinOverlayItem pinItem = new PinOverlayItem(item.getPoint(), "Note", pinNoteText, item.getPointId());
						pinOverlay.addOverlay(pinItem);
					}
					
				}

			} else if (resultCode == RESULT_CANCELED) {

				if (data != null && data.hasExtra("pointIndex")) {

					int pointIndex = data.getIntExtra("pointIndex", -1);

					PinOverlayItem item = pinOverlay.getItem(pointIndex);
					pinOverlay.deleteOverlay(pointIndex);
					srvc_local.deletePoint((int) item.getPointId());	
					
					mapView.invalidate();
				}

			}

			break;
		
		case REQUEST_LOAD_TRIP:

			if (resultCode == RESULT_OK) {
				
				int trip_id = data.getIntExtra("trip_id", -1);				
				loadTrip(trip_id);
				
			} else {				
				Log.d(TAG, "Map - Upload request failed");
			}

			break;
			
		case REQUEST_PIN:
			break;
			
		case REQUEST_ENABLE_LOCATION_PROVIDER:
			break;
			
		}
		
		initMenu();
	}

	// MENU HANDLING ----------------------------------------------------------

	public boolean onCreateOptionsMenu(Menu menu) {

		menuItemRecord = menu.add("Record new");
		menuItemPauseResume = menu.add("Pause");
		menuItemStop = menu.add("Stop");
		menuItemTrips = menu.add("Trips");
		menuItemUpload = menu.add("Upload");
		menuItemExit = menu.add("Exit");		
		isMenuCreated = true;
		
		initMenu();

		return true;
	}
	
	public boolean onOptionsItemSelected(MenuItem item) {
		 
		// Record
		if (item == menuItemRecord) {			
			if (locationProvider == null) {
				// No location provider available
				requestUserEnableGPSProvider(true);	
				return true;
			}			
			startActivityForResult(new Intent(this, CreateTripActivity.class), REQUEST_CREATE_TRIP);			
		} 
		// Pause / Resume
		else if (item == menuItemPauseResume) {
			if (currentState.getState() == CurrentState.RECORD) {
				currentState.setState(CurrentState.PAUSED);
			} else if (currentState.getState() == CurrentState.PAUSED) {
				if (locationProvider == null) {
					// No location provider available
					requestUserEnableGPSProvider(true);	
					return true;
				}	
				currentState.setState(CurrentState.RECORD);
			}
		}
		// Stop
		else if (item == menuItemStop) {
			currentState.setState(CurrentState.VIEW);
			Toast.makeText(getApplicationContext(), "The tour has been saved", Toast.LENGTH_SHORT).show();	
			printTrips();
		} 
		// Upload
		else if (item == menuItemUpload) {
			startActivityForResult(new Intent(this, UploadActivity.class), REQUEST_UPLOAD);
		}
		// Show Tours
		else if (item == menuItemTrips) {
			startActivityForResult(new Intent(this, TripLoadActivity.class), REQUEST_LOAD_TRIP);
		}
		// Exit
		else if (item == menuItemExit) {
			System.runFinalizersOnExit(true);
			System.exit(0);
		}
		
		return true;
	}	
	
	// GESTURE HANDLING -------------------------------------------------------
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		this.gestureDetector.onTouchEvent(ev);
		return super.dispatchTouchEvent(ev);
	}

	public boolean onDoubleTap(MotionEvent e) {

		// Add pin-point, if recording or paused
		
		if (currentState.getState() == CurrentState.RECORD ||
			currentState.getState() == CurrentState.PAUSED) {

			GeoPoint p = mapView.getProjection().fromPixels((int) e.getX(), (int) e.getY());
			Intent pinIntent = new Intent(this, CreatePinActivity.class);
			pinIntent.putExtra("action", CreatePinActivity.ACTION_CREATE);
			pinIntent.putExtra("longitude", p.getLongitudeE6());
			pinIntent.putExtra("latitude", p.getLatitudeE6());
			startActivityForResult(pinIntent, REQUEST_CREATE_PIN);
			
		}

		return false;
	}

	public boolean onDoubleTapEvent(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}

	public boolean onSingleTapConfirmed(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}

	public boolean onDown(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}

	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
		// TODO Auto-generated method stub
		return false;
	}

	public void onLongPress(MotionEvent e) {
		// TODO Auto-generated method stub

	}

	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
		// TODO Auto-generated method stub
		return false;
	}

	public void onShowPress(MotionEvent e) {
		// TODO Auto-generated method stub

	}

	public boolean onSingleTapUp(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}

	
	// DEBUG ------------------------------------------------------------------
	
	private void printTrips() {
		
		// Print trips to console
		
		if (!D) return;
		
		Log.i(TAG, "Map - TRIPS START");
		
		List<Trip> trips = srvc_local.getTrips(true);		
		for (Trip trip: trips) {			
			Log.i(TAG, "Map - tripId=" + trip.getTripId() + ", name=" + trip.getTripName() 
					+ ", pathPoints=" + srvc_local.getPoints(trip.getTripId(), PointType.PATH).size() 
					+ ", pinPoints=" + srvc_local.getPoints(trip.getTripId(), PointType.PIN).size());
		}		
		
		Log.i(TAG, "Map - TRIPS END");
	}
	
}